/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "config.h"
#include "logger.h"
#include "versioninformation.h"
#include "fsconstants.h"
#include "ircloggerlist.h"

#include <QFile>
#include <QListIterator>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonParseError>

constexpr const char CFG_LOG_LEVEL[]            = "logLevel";
constexpr const char CFG_LOG_FILE[]             = "logFile";
constexpr const char CFG_LOG_FILE_DATE_FORMAT[] = "logFileDateFormat";
constexpr const char CFG_WORKING_DIRECTORY[]    = "workingDirectory";
constexpr const char CFG_USE_UTC_TIME[]         = "useUTC";
constexpr const char CFG_IRC_ADMIN_TIMEOUT[]    = "ircabotAdminAuthorizationTimeout";

constexpr const char CFG_WEBUI[]           = "webUI";
constexpr const char CFG_WEBUI_BASE_URLS[] = "baseUrls";
constexpr const char CFG_WEBUI_ADDRESS[]   = "address";
constexpr const char CFG_WEBUI_PORT[]      = "port";
constexpr const char CFG_WEBUI_THREADS[]   = "threads";

constexpr const char CFG_NETWORKS[]              = "networks";
constexpr const char CFG_NETWORK_IRCABOT_ADMIN[] = "ircabotAdmin";
constexpr const char CFG_NETWORK_NETWORKNAME[]   = "networkName";
constexpr const char CFG_NETWORK_NICKNAME[]      = "nickname";
constexpr const char CFG_NETWORK_PASSWORD[]      = "password";
constexpr const char CFG_NETWORK_IDENTIFY_FMT[]  = "identifyFormat";
constexpr const char CFG_NETWORK_HOST[]          = "host";
constexpr const char CFG_NETWORK_PORT[]          = "port";
constexpr const char CFG_NETWORK_SSL[]           = "ssl";
constexpr const char CFG_NETWORK_IDENT[]         = "ident";
constexpr const char CFG_NETWORK_REAL_NAME[]     = "realName";
constexpr const char CFG_NETWORK_AUTORECONNECT[] = "autoReconnect";
constexpr const char CFG_NETWORK_VOICEGATE[]     = "voiceGate";
constexpr const char CFG_NETWORK_CHANNELS[]      = "channels";
constexpr const char CFG_NETWORK_PRIVACY[]       = "privacy";
constexpr const char CFG_NETWORK_BRIDGES[]       = "bridges";

constexpr const char CFG_VOICEGATE[]                = "voiceGate";
constexpr const char CFG_VOICEGATE_DIFFICULTY[]     = "difficulty";
constexpr const char CFG_VOICEGATE_ONLY_NUMBERS[]   = "onlyNumbers";
constexpr const char CFG_VOICEGATE_CACHE_SIZE[]     = "cacheSize";
constexpr const char CFG_VOICEGATE_CASE_SENSITIVE[] = "caseSensitive";

QString Config::m_configurationFilePath;
QString Config::m_logFilePath;
QString Config::m_logFileDateFormat = "yyyy-MM-dd hh:mm:ss";
bool Config::m_utcTime = false;
uint Config::m_ircAdminAuthorizationTimeout = 120;
QString Config::m_workingDirectory = "";

QStringList Config::m_webUiBaseUrls;
QString Config::m_webUiAddress;
quint16 Config::m_webUiPort = 0;
uint Config::m_webUiThreads = 0;

QList<QSharedPointer<Config::Network>> Config::m_networks;
Config::VoiceGate Config::m_voiceGate;

bool Config::initFromFile(QString *errorString)
{
    if (configurationFilePath().isEmpty())
    {
        if (errorString) *errorString = "undefined!";

        return false;
    }

    QFile file( configurationFilePath() );
    if (not file.open(QIODevice::ReadOnly))
    {
        if (errorString) *errorString = "Can't open file";

        return false;
    }

    QJsonParseError jerr;
    QJsonObject jcfg = QJsonDocument::fromJson(file.readAll(), &jerr).object();
    file.close();

    if (jerr.error != QJsonParseError::NoError)
    {
        if (errorString) *errorString = "Invalid JSON";

        return false;
    }

    return initFromJson(jcfg, errorString);
}

bool Config::initFromJson(const QJsonObject &config, QString* errorString)
{
    logger::level = logger::levelEnum(config.value(CFG_LOG_LEVEL).toString());

    m_logFilePath = config.value(CFG_LOG_FILE).toString();

    m_workingDirectory = config.value(CFG_WORKING_DIRECTORY).toString();

    if (not m_workingDirectory.isEmpty() /*empty = folder with executable*/ and not m_workingDirectory.endsWith(FS_NATIVE_SEPARATOR))
    {
        m_workingDirectory.push_back(FS_NATIVE_SEPARATOR);
    }

    m_utcTime = config.value(CFG_USE_UTC_TIME).toBool(false);

    m_ircAdminAuthorizationTimeout = config.value(CFG_IRC_ADMIN_TIMEOUT).toInt(120);

    QString buffer = config.value(CFG_LOG_FILE_DATE_FORMAT).toString();
    if (not buffer.isEmpty())
    {
        m_logFileDateFormat = buffer;
    }

    QJsonObject webUi = config.value(CFG_WEBUI).toObject();
    m_webUiAddress  = webUi.value(CFG_WEBUI_ADDRESS) .toString("127.0.0.1");
    m_webUiPort     = webUi.value(CFG_WEBUI_PORT)    .toInt(7666);
    m_webUiThreads  = webUi.value(CFG_WEBUI_THREADS) .toInt(0); // if <1, then system number

    m_webUiBaseUrls.clear();
    QJsonArray baseUrls = webUi.value(CFG_WEBUI_BASE_URLS).toArray();
    for (const auto& jurl: baseUrls)
    {
        auto url = jurl.toString();
        if (not url.startsWith("http"))
        {
            qCritical() << "Base url" << url << "not starts with HTTP prefix";
        }
        else
        {
            url.remove(' ').remove('\r').remove('\n');
            url.push_back(not url.endsWith('/') ? "/" : "");
            m_webUiBaseUrls.push_back(url);
        }
    }
    if (m_webUiBaseUrls.isEmpty())
    {
        m_webUiBaseUrls.push_back("http://"+m_webUiAddress+":"+QString::number(m_webUiPort)+"/");
    }

    QJsonObject voiceGatej = config.value(CFG_VOICEGATE).toObject();
    m_voiceGate.captchaCacheSize = voiceGatej.value(CFG_VOICEGATE_CACHE_SIZE).toInt(2048);
    m_voiceGate.captchaCaseSensitive = voiceGatej.value(CFG_VOICEGATE_CASE_SENSITIVE).toBool(false);
    m_voiceGate.captchaOnlyNumbers = voiceGatej.value(CFG_VOICEGATE_ONLY_NUMBERS).toBool(false);
    m_voiceGate.captchaDifficulty = voiceGatej.value(CFG_VOICEGATE_DIFFICULTY).toInt(1);
    if (m_voiceGate.captchaDifficulty > 2)
    {
        qWarning() << "Voice gate config: Captcha difficulty max is 2. Set to 2";
        m_voiceGate.captchaDifficulty = 2;
    }
    if (m_voiceGate.captchaDifficulty < 0)
    {
        qWarning() << "Voice gate config: Captcha difficulty min is 0. Set to 0";
        m_voiceGate.captchaDifficulty = 0;
    }

    QJsonArray jnetworks = config.value(CFG_NETWORKS).toArray();
    if (jnetworks.isEmpty())
    {
        if (errorString) *errorString = "Networks is undefined";

        return false;
    }

    uint unnamedNetworkCounter = 0;
    for (const auto& jnet: jnetworks)
    {
        auto jnetobj = jnet.toObject();
        if (jnetobj.isEmpty()) continue;

        QSharedPointer<Network> network (new Network);

        network->ircabotAdmin   = jnetobj.value(CFG_NETWORK_IRCABOT_ADMIN)  .toString();
        network->networkName    = jnetobj.value(CFG_NETWORK_NETWORKNAME)    .toString("Unnamed-" + QString::number(unnamedNetworkCounter++));
        network->nickname       = jnetobj.value(CFG_NETWORK_NICKNAME)       .toString(info::NAME);
        network->password       = jnetobj.value(CFG_NETWORK_PASSWORD)       .toString();
        network->identifyFormat = jnetobj.value(CFG_NETWORK_IDENTIFY_FMT)   .toString("PRIVMSG NickServ identify $nickname $password"); // from libircclient/network->cpp
        network->ident          = jnetobj.value(CFG_NETWORK_IDENT)          .toString(QString(info::NAME) + " " + QString(info::VERSION));
        network->host           = jnetobj.value(CFG_NETWORK_HOST)           .toString();
        network->realName       = jnetobj.value(CFG_NETWORK_REAL_NAME)      .toString(info::SOURCE_CODE);
        network->autoReconnect  = jnetobj.value(CFG_NETWORK_AUTORECONNECT)  .toBool(true);
        network->voiceGate      = jnetobj.value(CFG_NETWORK_VOICEGATE)      .toBool(true);
        network->ssl            = jnetobj.value(CFG_NETWORK_SSL)            .toBool(false);
        network->port           = jnetobj.value(CFG_NETWORK_PORT)           .toInt(network->ssl ? 6697 : 6667);

        QJsonArray jchannels = jnetobj.value(CFG_NETWORK_CHANNELS).toArray();
        for (const auto& jchan: jchannels)
        {
            QString channel = jchan.toString();
            channel.remove(' ');
            if (channel.isEmpty()) continue;
            if (not channel.startsWith('#'))
            {
                channel.push_front('#');
            }
            network->channels.push_back(channel);

            IRCLoggerList::get(network->networkName, channel); // Create logger instanse for date indexing at start
        }
        if (network->channels.isEmpty())
        {
            qWarning().noquote() << "No channels defined for network " + network->networkName;
        }

        if (network->host.isEmpty())
        {
            if (errorString) *errorString = "Host undefined for network " + network->networkName;

            return false;
        }

        network->hideMessageByLeadingDot = jnetobj.value(CFG_NETWORK_PRIVACY).toBool(true);
        QJsonArray jbridges = jnetobj.value(CFG_NETWORK_BRIDGES).toArray();
        for (const auto& jnick: jbridges)
        {
            const QString nick = jnick.toString().trimmed();
            if (nick.isEmpty()) continue;
            network->bridgeNicknames.push_back(nick);
        }

        m_networks.push_back(network);
    }

    return true;
}

bool Config::updateConfigWithCurrentData(QString *errorString)
{
    QFile file( configurationFilePath() );
    if (not file.open(QIODevice::WriteOnly))
    {
        if (errorString) *errorString = "Can't open file";

        return false;
    }

    QJsonObject config;
    config.insert(CFG_LOG_LEVEL,            logger::levelString(logger::level));
    config.insert(CFG_LOG_FILE,             logFilePath());
    config.insert(CFG_LOG_FILE_DATE_FORMAT, logFileDateFormat());
    config.insert(CFG_WORKING_DIRECTORY,    workingDirectory());
    config.insert(CFG_USE_UTC_TIME,         utcTime());
    config.insert(CFG_IRC_ADMIN_TIMEOUT,    ircAdminAuthorizationTimeout());

    QJsonObject webui;
    webui.insert(CFG_WEBUI_ADDRESS, webUiAddress());
    webui.insert(CFG_WEBUI_PORT, static_cast<int>(webUiPort()));
    webui.insert(CFG_WEBUI_THREADS, webUiThreads());
    QJsonArray baseUrls;
    QStringListIterator baseUrlsIter(m_webUiBaseUrls);
    while(baseUrlsIter.hasNext())
    {
        const auto url = baseUrlsIter.next();
        baseUrls.push_back(url);
    }
    webui.insert(CFG_WEBUI_BASE_URLS, baseUrls);

    config.insert(CFG_WEBUI, webui);

    QJsonObject voiceGatej;
    voiceGatej.insert(CFG_VOICEGATE_DIFFICULTY, m_voiceGate.captchaDifficulty);
    voiceGatej.insert(CFG_VOICEGATE_CACHE_SIZE, m_voiceGate.captchaCacheSize);
    voiceGatej.insert(CFG_VOICEGATE_CASE_SENSITIVE, m_voiceGate.captchaCaseSensitive);
    voiceGatej.insert(CFG_VOICEGATE_ONLY_NUMBERS, m_voiceGate.captchaOnlyNumbers);

    config.insert(CFG_VOICEGATE, voiceGatej);

    QListIterator<QSharedPointer<Network>> networksIter(m_networks);
    QJsonArray networks;
    while (networksIter.hasNext())
    {
        auto network = networksIter.next();

        QJsonObject jnet;
        jnet.insert(CFG_NETWORK_IRCABOT_ADMIN, network->ircabotAdmin);
        jnet.insert(CFG_NETWORK_NETWORKNAME, network->networkName);
        jnet.insert(CFG_NETWORK_NICKNAME, network->nickname);
        jnet.insert(CFG_NETWORK_PASSWORD, network->password);
        jnet.insert(CFG_NETWORK_IDENTIFY_FMT, network->identifyFormat);
        jnet.insert(CFG_NETWORK_HOST, network->host);
        jnet.insert(CFG_NETWORK_PORT, static_cast<int>(network->port));
        jnet.insert(CFG_NETWORK_SSL, network->ssl);
        jnet.insert(CFG_NETWORK_IDENT, network->ident);
        jnet.insert(CFG_NETWORK_REAL_NAME, network->realName);
        jnet.insert(CFG_NETWORK_AUTORECONNECT, network->autoReconnect);
        jnet.insert(CFG_NETWORK_VOICEGATE, network->voiceGate);

        QJsonArray jchannels;
        QStringListIterator chanIter(network->channels);
        while (chanIter.hasNext())
        {
            jchannels.push_back(chanIter.next());
        }
        jnet.insert(CFG_NETWORK_CHANNELS, jchannels);

        jnet.insert(CFG_NETWORK_PRIVACY, network->hideMessageByLeadingDot);
        QJsonArray jbridges;
        QStringListIterator bridgeIter(network->bridgeNicknames);
        while (bridgeIter.hasNext())
        {
            jbridges.push_back(bridgeIter.next());
        }
        jnet.insert(CFG_NETWORK_BRIDGES, jbridges);

        networks.push_back(jnet);
    }
    config.insert(CFG_NETWORKS, networks);

    QJsonObject generated;
    generated.insert("name", info::NAME);
    generated.insert("verion", info::VERSION);
    generated.insert("date", QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
    config.insert("generated", generated);

    QByteArray bytes = QJsonDocument(config).toJson(QJsonDocument::JsonFormat::Indented);
    auto written = file.write(bytes);
    file.close();
    if (written != bytes.size())
    {
        if (errorString) *errorString = "Writing failed: written " + QString::number(written) + " of " + QString::number(bytes.size()) + " bytes";

        return false;
    }
    return true;
}
